/**
 * LocationAPITools library, version 1.0.2
 * 
 * WWW: http://code.google.com/p/locationapitools
 *   
 * Summary:
 *   A collection of classes that simplify the development of location-aware mobile
 *   applications using the JSR-179 Location API on the J2ME (CLDC/MIDP) platform,
 *   e.g. on GPS-equipped smartphones.
 * 
 * Author:
 *   Matthias Stevens (Sony CSL Paris / Vrije Universiteit Brussel)
 *   Contact: matthias.stevens@gmail.com
 *  
 * Description:
 *   The primary use case is a location tracker that stores tracks in KML
 *   (cfr. Google Earth/Maps), CSV or plain text format.
 * 
 *   Package "com.sony.csl.j2me.locationapitools.demo" contains a MIDlet class that
 *   demonstrates how LocationAPITools can be used to create simple GPS tracker
 *   application that produces KML files. For examples of more advanced uses of this
 *   library refer to the NoiseTube project (see below, look for "NoiseTube MobileNoiseTagger").
 * 
 *   The code is known to be working on the following Symbian OS-based platforms:
 *     * S60 3rd Edition Feature Pack 1 (Symbian OS 9.2), e.g.: Nokia N95 and Nokia N95 8GB smartphones
 *     * S60 3rd Edition Feature Pack 2 (Symbian OS 9.3) 
 *   Although this is untested, the code should also work on other (non-Symbian/S60)
 *   J2ME CLDC/MIDP platforms/devices, as long as the JSR-179 Location API is supported
 *   on the platform/device in question.
 * 
 *   This library was developed within the scope of the NoiseTube project at
 *   Sony Computer Science Laboratory (CSL) Paris, for more information please refer to:
 *     * http://noisetube.net
 *     * http://code.google.com/p/noisetube
 *     * http://www.csl.sony.fr 
 *    
 * License: 
 *   Copyright 2008-2009 Sony CSL Paris
 *   Licensed under the Apache License, Version 2.0 (the "License"); you may not use
 *   this file except in compliance with the License. You may obtain a copy of the
 *   License at http://www.apache.org/licenses/LICENSE-2.0.
 *   Unless required by applicable law or agreed to in writing, software distributed
 *   under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
 *   CONDITIONS OF ANY KIND, either express or implied. See the License for the
 *   specific language governing permissions and limitations under the License.
 */

package com.sony.csl.j2me.locationapitools.tracking;

import java.io.OutputStreamWriter;

import javax.microedition.location.Location;
import javax.microedition.location.LocationProvider;

import com.sony.csl.j2me.locationapitools.LocationProxy;
import com.sony.csl.j2me.locationapitools.LocationProxyListener;
import com.sony.csl.j2me.util.StringUtils;
import com.sony.csl.j2me.io.FileAccess;


/**
 * An abstract class that implements a location tracker. Instances of subclasses of this class
 * act a listeners on LocationProxy class and write received location updates to file.
 * 
 * @author mstevens (Sony CSL Paris / Vrije Universiteit Brussel)
 *
 */
public abstract class TrackWriter implements LocationProxyListener
{
	protected String characterEncoding = null;
	
	protected String trackName;
	protected String trackFileFolderPath;
	protected String trackFilePath;
	protected boolean tracksInvalids;
	
	protected LocationProxy locationProxy;
	
	private boolean tracking = false;
	private boolean paused = false;
	
	//Track (meta)data:
	protected long trackStartTime;
	protected Location firstLocation;
	protected long firstLocationReceivalTime;
	protected Location firstValidLocation;
	protected Location lastLocation;
	protected long lastLocationReceivalTime;
	protected Location lastValidLocation;
	protected int numberOfLocations;
	protected int numberOfValidLocations;
	protected float coveredDistance;
	protected double minLongitude;
	protected double maxLongitude;
	protected double minLatitude;
	protected double maxLatitude;
	protected float minAltitude;
	protected float maxAltitude;
	
	private OutputStreamWriter trackWriter = null;
	
	public TrackWriter(LocationProxy locationProxy, String trackFileFolderPath, String trackName) throws Exception
	{
		this(locationProxy, trackFileFolderPath, trackName, false);
	}
	
	public TrackWriter(LocationProxy locationProxy, String trackFileFolderPath, String trackName, boolean tracksInvalids) throws Exception
	{
		if(locationProxy == null)
			throw new NullPointerException("LocationProxy cannot be null");
		this.trackName = trackName;
		this.trackFileFolderPath = FileAccess.addFilePathPrefix(FileAccess.addTrailingSeparatorToFolderPath(trackFileFolderPath));
		if(!FileAccess.doesFolderExist(trackFileFolderPath, true)) //check if containing folders exists, try to create if not
			throw new Exception("The specified folder path (" + trackFileFolderPath + ") is invalid!");
		this.tracksInvalids = tracksInvalids;
		this.locationProxy = locationProxy;
	}
	
	/**
	 * @return the characterEncoding
	 */
	public String getCharacterEncoding()
	{
		return characterEncoding;
	}

	/**
	 * @param characterEncoding the characterEncoding to set
	 */
	public void setCharacterEncoding(String characterEncoding)
	{
		this.characterEncoding = characterEncoding;
	}
		
	/**
	 * @return the trackFilePath
	 */
	public String getTrackFilePath()
	{
		return trackFilePath;
	}

	/**
	 * @return the trackName
	 */
	public String getTrackName()
	{
		return trackName;
	}

	/**
	 * @return the tracksInvalids
	 */
	public boolean isTracksInvalids()
	{
		return tracksInvalids;
	}

	/**
	 * @return the locationProxy
	 */
	public LocationProxy getLocationProxy()
	{
		return locationProxy;
	}

	/**
	 * @return the tracking
	 */
	public boolean isTracking()
	{
		return tracking;
	}

	/**
	 * @return the paused
	 */
	public boolean isPaused()
	{
		return paused;
	}

	/**
	 * @return the firstLocation
	 */
	public Location getFirstLocation()
	{
		return firstLocation;
	}

	/**
	 * @return the firstLocationReceivalTime
	 */
	public long getFirstLocationReceivalTime()
	{
		return firstLocationReceivalTime;
	}

	/**
	 * @return the firstValidLocation
	 */
	public Location getFirstValidLocation()
	{
		return firstValidLocation;
	}

	/**
	 * @return the lastLocation
	 */
	public Location getLastLocation()
	{
		return lastLocation;
	}

	/**
	 * @return the lastLocationReceivalTime
	 */
	public long getLastLocationReceivalTime()
	{
		return lastLocationReceivalTime;
	}

	/**
	 * @return the lastValidLocation
	 */
	public Location getLastValidLocation()
	{
		return lastValidLocation;
	}

	/**
	 * @return the numberOfLocations
	 */
	public int getNumberOfLocations()
	{
		return numberOfLocations;
	}

	/**
	 * @return the numberOfValidLocations
	 */
	public int getNumberOfValidLocations()
	{
		return numberOfValidLocations;
	}

	/**
	 * @return the coveredDistance
	 */
	public float getCoveredDistance()
	{
		return coveredDistance;
	}

	/**
	 * @return the minLongitude
	 */
	public double getMinLongitude()
	{
		return minLongitude;
	}

	/**
	 * @return the maxLongitude
	 */
	public double getMaxLongitude()
	{
		return maxLongitude;
	}

	/**
	 * @return the minLatitude
	 */
	public double getMinLatitude()
	{
		return minLatitude;
	}

	/**
	 * @return the maxLatitude
	 */
	public double getMaxLatitude()
	{
		return maxLatitude;
	}

	/**
	 * @return the minAltitude
	 */
	public float getMinAltitude()
	{
		return minAltitude;
	}

	/**
	 * @return the maxAltitude
	 */
	public float getMaxAltitude()
	{
		return maxAltitude;
	}

	protected void reset()
	{
		trackStartTime = 0;
		firstLocation = null;
		firstLocationReceivalTime = 0;
		firstValidLocation = null;
		lastLocation = null;
		lastLocationReceivalTime = 0;
		lastValidLocation = null;
		numberOfLocations = 0;
		numberOfValidLocations = 0;
		coveredDistance = 0.0f;
		minLongitude = 180.0;
		maxLongitude = -180.0;
		minLatitude = 90.0;
		maxLatitude = -90.0;
		minAltitude = Float.MAX_VALUE;
		maxAltitude = Float.MIN_VALUE;
	}
	
	public final void startTracking() throws Exception
	{				
		if(	locationProxy.getProviderState() == LocationProvider.AVAILABLE ||
			locationProxy.getProviderState() == LocationProvider.TEMPORARILY_UNAVAILABLE)
		{
			try
			{	
				reset();
				initializeBeforeStart();
				trackStartTime = System.currentTimeMillis();
				trackFilePath = trackFileFolderPath + FileAccess.makeValidFileName(StringUtils.formatDateTime(trackStartTime, "-", "", "T") + " " + trackName + "." + getFileExtension());
		        trackWriter = FileAccess.getFileWriter(trackFilePath, FileAccess.FILE_EXISTS_STRATEGY_CREATE_RENAMED_FILE, FileAccess.FILE_DOES_NOT_EXIST_STRATEGY_CREATE, characterEncoding); 
				if(trackWriter == null)
					throw new Exception("trackWriter is null");
		        tracking = true;
				writeHeader();
				if(locationProxy.getProviderState() == LocationProvider.TEMPORARILY_UNAVAILABLE)
					pauseTracking(); //start paused
				locationProxy.addListener(this); //!!!
			}
			catch(Exception e)
			{
				throw new Exception("Could not open file for writing [" + e.getMessage() + "].");
			}
		}			
		else //if(locationProxy.getProviderState() == LocationProvider.OUT_OF_SERVICE)
			throw new Exception("The LocationProvider to which the LocationProxy is connected is unavailable.");
	}
	
	public final void pauseTracking()
	{
		pauseTracking(null);
	}
	
	public final void pauseTracking(String reason)
	{
		if(tracking && !paused)
		{
			writePaused(reason);
			paused = true;
		}
	}
	
	protected abstract void writePaused(String reason);
	
	public final void resumeTracking()
	{
		if(tracking && paused)
		{
			writeResumed();
			paused = false;
		}
	}
	
	protected abstract void writeResumed();
	
	public final void stopTracking()
	{
		if(tracking)
		{
			try
			{
				locationProxy.removeListener(this); //!!!
				writeFooter();	
				trackWriter.close();
				trackWriter = null;
			}
			catch(Exception e)
			{	//it makes no sense to throw this further as the application is most likely closing already
				System.err.println("Exception upon stopping tracking/closing the file [" + e.getMessage() + "].");
			}
			finally
			{
				tracking = false;
				paused = false;
			}
		}
	}
	
	protected final void writeToFile(String stringToWrite) //throws Exception
	{
		if(tracking)
		{
			try
			{
	    		trackWriter.write(stringToWrite);
	    		trackWriter.flush();
			}
			catch(Exception e)
			{
				stopTracking();
				//throw new Exception("Failed to write to trackfile [" + e.getMessage() + "], tracking stopped.");
			}
		}
	}
	
	/**
	 * Gets the file extension (e.g.: "txt") that is used on the files the TrackWriter creates
	 * 
	 * @return a String containing the file extension (without the '.') 
	 */
	protected abstract String getFileExtension();
	
	/**
	 * Allows subclasses to add initializations that should take place before tracking starts.
	 */	
	protected abstract void initializeBeforeStart();
	
	/**
	 * Writes the header of the file. 
	 */
	protected abstract void writeHeader();
	
	/**
	 * Writes a new location to the file. 
	 */
	protected abstract void writeLocation(Location location);
	
	/**
	 * Writes the footer of the file. 
	 */
	protected abstract void writeFooter();
	
	/** 
	 * @see com.sony.csl.j2me.locationapitools.LocationProxyListener#locationUpdated(com.sony.csl.j2me.locationapitools.LocationProxy, javax.microedition.location.LocationProvider, javax.microedition.location.Location)
	 */
	public final void locationUpdated(LocationProxy proxy, LocationProvider provider, Location location)
	{
		if(tracking && !paused)
		{
			if(location.isValid())
			{
				if(firstValidLocation == null)
					firstValidLocation = location;
				numberOfValidLocations++;
				if(lastValidLocation != null)
					coveredDistance += lastValidLocation.getQualifiedCoordinates().distance(location.getQualifiedCoordinates());
				if(location.getQualifiedCoordinates().getLongitude() < minLongitude)
					minLongitude = location.getQualifiedCoordinates().getLongitude();
				if(location.getQualifiedCoordinates().getLongitude() > maxLongitude)
					maxLongitude = location.getQualifiedCoordinates().getLongitude();				
				if(location.getQualifiedCoordinates().getLatitude() < minLatitude)
					minLatitude = location.getQualifiedCoordinates().getLatitude();
				if(location.getQualifiedCoordinates().getLatitude() > maxLatitude)
					maxLatitude = location.getQualifiedCoordinates().getLatitude();
				if(!Float.isNaN(location.getQualifiedCoordinates().getAltitude()))
				{
					if(location.getQualifiedCoordinates().getAltitude() < minAltitude)
						minAltitude = location.getQualifiedCoordinates().getAltitude();
					if(location.getQualifiedCoordinates().getAltitude() > maxAltitude)
						maxAltitude = location.getQualifiedCoordinates().getAltitude();
				}
			}
			if(firstLocation == null)
			{
				firstLocation = location;
				firstLocationReceivalTime = System.currentTimeMillis();
			}
			numberOfLocations++;
			writeLocation(location);
			lastLocation = location;
			lastLocationReceivalTime = System.currentTimeMillis();
			if(location.isValid())
				lastValidLocation = location;
		}
	}
	
	/**
	 * @see com.sony.csl.j2me.locationapitools.LocationProxyListener#proxyConnected(com.sony.csl.j2me.locationapitools.LocationProxy, javax.microedition.location.LocationProvider)
	 */
	public void proxyConnected(LocationProxy proxy, LocationProvider provider) {}

	/**
	 * @see com.sony.csl.j2me.locationapitools.LocationProxyListener#proxyDisconnected(com.sony.csl.j2me.locationapitools.LocationProxy)
	 */
	public final void proxyDisconnected(LocationProxy proxy)
	{
		if(tracking)
			stopTracking();
	}
	
	/**
	 * @see com.sony.csl.j2me.locationapitools.LocationProxyListener#locationProviderStateChanged(com.sony.csl.j2me.locationapitools.LocationProxy, javax.microedition.location.LocationProvider, int)
	 */
	public void locationProviderStateChanged(LocationProxy proxy, LocationProvider provider, int newState)
	{
		writeProviderStateChanged(provider, newState);
		if(newState == LocationProvider.OUT_OF_SERVICE)
			if(tracking)
				stopTracking();
		//if(newState == LocationProvider.TEMPORARILY_UNAVAILABLE)
		//	if(tracking && !paused)
		//		pauseTracking();
		if(newState == LocationProvider.AVAILABLE)
			if(tracking && paused)
				resumeTracking();
	}
	
	protected abstract void writeProviderStateChanged(LocationProvider provider, int newState);
	
	/**
	 * @see com.sony.csl.j2me.locationapitools.LocationProxyListener#firstLocationFound(com.sony.csl.j2me.locationapitools.LocationProxy, javax.microedition.location.LocationProvider, javax.microedition.location.Location)
	 */
	public void firstLocationFound(LocationProxy proxy, LocationProvider provider, Location location) {}
	
	public boolean receivesInvalidLocations()
	{
		return tracksInvalids;
	}
	
}